package boxync;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;

public abstract class BoxyncThread extends Thread {
    /**
     * Socket client
     */
    public Socket socket;

    /**
     * PrintWriter untuk menulis ke Client
     */
    public ObjectOutputStream out;

    /**
     * BufferedReader untuk membaca dari Client
     */
    public ObjectInputStream in;
    
    /**
     * Tanda bahwa thread sudah (harus) distop
     */
    public boolean stopped;

    /**
     * Direktori repositori Client di Server
     */
    public BoxyncDir dir;
    
    public BoxyncThread(BoxyncDir _dir) {
        dir = _dir;
        stopped = false;
    }
    
    public BoxyncThread(Socket _socket, BoxyncDir _dir) {
        socket = _socket;
        dir = _dir;
        stopped = false;
        
        try {
            out = new ObjectOutputStream(socket.getOutputStream());
            in = new ObjectInputStream(socket.getInputStream());
        } catch (IOException e) {
            System.out.println("IOExc in BoxyncThread.BoxyncThread()");
        }
    }
    
    /**
     * Mensync working directory di harddisk dengan working directory di runtime, yaitu:
     * Apabila di runtime terdapat suatu file dan di winexplorer tidak terdapat file tersebut, 
     * file tersebut akan dimark delete di runtime.
     * Apabila di winexplorer terdapat suatu file dan di runtime tidak terdapat file tersebut,
     * file tersebut akan dimasukkan ke runtime.
     */
    public void syncWithExplorer() {
        ArrayList<FileInfo> explFI = (new BoxyncDir(null, dir.file)).getFileList();
        ArrayList<FileInfo> runtimeFI = dir.getFileList();

        for (FileInfo fi : runtimeFI) {
            if (!explFI.contains(fi)) {
                fi.deleted = 1;
                fi.timeStamp = new Date();
                fi.timeStamp.setYear(fi.timeStamp.getYear() + 1900);
            }
        }

        for (FileInfo fi : explFI) {
            int rfIdx = runtimeFI.indexOf(fi);
            if (rfIdx < 0) {
                dir.insertFile(fi);
            } else {
                FileInfo rfi = runtimeFI.get(rfIdx);
                rfi.deleted = 0;
                rfi.timeStamp = fi.timeStamp;
            }
        }
    }
        
        
    
    /**
     * Bertukar update dengan Client, dimulai dengan Server mendorong update,
     * kemudian Server meminta update
     * @throws BoxyncException 
     */
    public abstract void exchangeUpdates() throws BoxyncException;
    
    /**
     * Mengirim list BoxyncFile ke Client untuk sync, newline separated
     */
    public void pushUpdates() {
        write(BoxyncProtocol.INIT_UPDATE);

        String upd = dir.toString();
        if (!upd.isEmpty()) {
            String[] upds = upd.split("\n");
            for (String fileInfo : upds) {
                write(fileInfo);
            }
        }

        write(BoxyncProtocol.END_UPDATE);
    }

    /**
     * Meminta list BoxyncFile dari Client untuk sync
     * @return String list BoxyncFile dari BoxyncDir client, newline separated
     * @throws BoxyncException
     */
    public ArrayList<String> pullUpdates() throws BoxyncException {
        String code = null;
        String file = null;
        ArrayList<String> updates = new ArrayList<String>();

        try {
            code = read();
        
            if (!code.equals(BoxyncProtocol.INIT_UPDATE))
                throw new BoxyncException(code, BoxyncProtocol.INIT_UPDATE);

            while (!(file = read()).equals(BoxyncProtocol.END_UPDATE)) {
                if (file.isEmpty())
                    continue;

                FileInfo fi = dir.searchFile(FileInfo.getFileName(file));
                if (fi != null) {
                    if (!FileInfo.isDeleted(file)) {
                        if (fi.compareTo(file) < 0) {
                            if (!FileInfo.isDirectory(FileInfo.getFileName(file)))
                                updates.add(file);
                        } else {
                            // do nothing
                        }
                    } else {
                        if (fi.deleted == 0) {
                            if (fi.compareTo(file) < 0) {
                                updates.add(file);
                            }
                        } else {
                            // do nothing
                        }
                    }
                } else {
                    updates.add(file);
                }
            }
        } catch (NullPointerException e) {
            System.err.println(e);
        }

        return updates;
    }

    public abstract void handleUpdate(String change);
    
    /**
     * Meminta file update
     * @param update String BoxyncFile yg diinginkan
     * @throws BoxyncException jika urutan message tidak sesuai protokol
     */
    public void requestDownload(String update) throws BoxyncException {
        try {
            write(BoxyncProtocol.REQUEST_UPDATE);
            write(update);
            
            String code = read();
            if (!code.equals(BoxyncProtocol.SEND_DATA))
                throw new BoxyncException(code, BoxyncProtocol.SEND_DATA);

            File f = new File(dir.file + FileInfo.getFileName(update));
            if (FileInfo.isDirectory(FileInfo.getFileName(update)))
                f.mkdirs();
            else {
                int size = Integer.parseInt(read());
                BufferedOutputStream bos = new BufferedOutputStream(
                        new FileOutputStream(f));
                int len;
                int total = 0;
                boolean stop = false;
                while (!stop) {
                    byte[] bytes;
                    if (total+512 < size) {
                        bytes = new byte[512];
                    }
                    else {
                        bytes = new byte[size - total];
                        stop = true;
                    }

                    total += 512;
                    len = in.read(bytes);
                    if (len == -1)
                        break;
                    else if (len > 0) {
                        bos.write(bytes);
                        bos.flush();
                    }
                }
                bos.close();
            }
            String time = FileInfo.getTimeStamp(update);
            f.setLastModified(new Date(FileInfo.getYear(time) - 1900, FileInfo.getMonth(time) - 1,
                    FileInfo.getDate(time), FileInfo.getHour(time), FileInfo.getMinute(time),
                    FileInfo.getSecond(time)).getTime());

            write(BoxyncProtocol.DATA_OK);
        } catch (NullPointerException e) {
            System.err.println(e);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
    
    public void handleDownload() throws BoxyncException {
        try {
            String file = BoxyncFile.getFileName(read());

            write(BoxyncProtocol.SEND_DATA);
            File f = new File(dir.file + file);
            if (!f.isDirectory()) {
                write(f.length()+"");
                    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
                    byte[] bytes = new byte[512];
                    int len;
                    while (bis.available() != 0) {
                        len = bis.read(bytes);
                        if (len == -1)
                            break;

                        byte[] b = new byte[len];
                        System.arraycopy(bytes, 0, b, 0, len);
                        out.write(b);
                        out.flush();
                    }
                    bis.close();
            }
            String code = read();
            if (!code.equals(BoxyncProtocol.DATA_OK))
                throw new BoxyncException(code, BoxyncProtocol.DATA_OK);
        } catch (NullPointerException e) {
            System.err.println(e);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
    
    /**
     * Mengembalikan hasil pembacaan in sebanyak satu baris dari Client dan mencetak ke System.out
     * @return satu baris String hasil pembacaan
     */
    public abstract String read();
    
    /**
     * Mengembalikan hasil pembacaan in sebanyak satu baris dari Client tanpa mencetak ke System.out
     * @return satu baris String hasil pembacaan
     */
    public abstract String readWOPrint();

    /**
     * Menulis String str ke socket milik Client dan mencetak str ke System.out
     * @param str String yg ingin ditulis
     */
    public void write(String str) {
        System.out.println("Write to " + socket.getPort() + ": " + str);
        try {
            out.writeObject(str);
            out.flush();
        } catch (IOException e) {
            System.err.println("IOExc in BoxyncThread.write()");
        }
    }
    
    /**
     * Menulis String str ke socket milik Client tanpa mencetak str ke System.out
     * @param str String yg ingin ditulis
     */
    public void writeWOPrint(String str) {
        try {
            out.writeObject(str);
            out.flush();
        } catch (IOException e) {
            System.err.println("IOExc in BoxyncThread.writeWOPrint()");
        }
    }
    
    public byte[] readBytes(int size) {
        byte[] buf = new byte[size];
        try {
            socket.getInputStream().read(buf);
        } catch (IOException e) {
            System.out.println("IOExc in BoxyncThread.readBytes()");
        }
        return buf;
    }
    
    public void writeBytes(byte[] bytes) throws BoxyncException {
        try {
            socket.getOutputStream().write(bytes);
        } catch (IOException e) {
            System.err.println("IOExc in BoxyncThread.writeBytes()");
        }
    }
}
